Skip to content

feat(fast-html): add AttributeMap class for automatic @attr definitions#7354

Draft
janechu wants to merge 12 commits intomainfrom
users/janechu/create-attribute-map
Draft

feat(fast-html): add AttributeMap class for automatic @attr definitions#7354
janechu wants to merge 12 commits intomainfrom
users/janechu/create-attribute-map

Conversation

@janechu
Copy link
Copy Markdown
Collaborator

@janechu janechu commented Mar 31, 2026

Pull Request

📖 Description

Adds an AttributeMap class to @microsoft/fast-html that automatically defines @attr properties on a custom element's class prototype based on the JSON schema generated by TemplateElement.

When attributeMap: "all" is configured for an element via TemplateElement.options(), AttributeMap inspects the schema after template processing and creates reactive properties for all leaf bindings — simple template expressions like {{foo}} or id="{{foo-bar}}" that have no nested properties, no array type, and no child element references.

Key behaviours:

  • Attribute name = binding key as written in the template (e.g. foo-bar)
  • Property name = attribute name with dashes removed (e.g. foo-barfoobar)
  • Skips non-leaf properties: paths like {{user.name}} result in user having sub-properties in the schema and are excluded
  • Skips existing accessors: properties already decorated with @attr or @observable are left untouched
  • Updates FASTElementDefinition: attributeLookup and propertyLookup are patched so attributeChangedCallback correctly delegates to the new AttributeDefinition
  • No decorator syntax required: uses Observable.defineProperty with an AttributeDefinition instance directly

Usage

TemplateElement.options({
    "my-element": {
        attributeMap: "all",
    },
});

This mirrors the existing ObserverMap integration pattern.

📑 Test Plan

13 Playwright tests were added across two spec files:

  • packages/fast-html/src/components/attribute-map.spec.ts — verifies accessor registration, dash-removal conversion, event handler exclusion, and FASTElementDefinition lookup updates
  • packages/fast-html/test/fixtures/attribute-map/attribute-map.spec.ts — end-to-end tests verifying template re-rendering when properties are set via JavaScript

All existing tests continue to pass.

✅ Checklist

General

  • I have included a change request file using $ npm run change
  • I have added tests for my changes.
  • I have tested my changes.
  • I have updated the project documentation to reflect my changes.
  • I have read the CONTRIBUTING documentation and followed the standards for this project.

⏭ Next Steps

  • Investigate whether observedAttributes can be updated after element registration to fully support the DOM attribute → property direction for elements registered before the template is processed

janechu and others added 4 commits March 30, 2026 20:50
AttributeMap inspects the JSON schema generated by TemplateElement for a
custom element and defines @attr properties on the class prototype for
all leaf-level bindings (simple {{foo}} and attribute {{bar}} paths that
have no nested properties, no type, and no anyOf).

- Add AttributeMap class in packages/fast-html/src/components/attribute-map.ts
  - Reads root properties from the Schema
  - Skips properties with nested 'properties', 'type', or 'anyOf' (not leaves)
  - Skips properties that already have an @attr or @observable accessor
  - Converts camelCase property names to dash-case (fooBar -> foo-bar)
  - Creates AttributeDefinition instances via Observable.defineProperty
  - Updates FASTElementDefinition.attributeLookup and propertyLookup
- Integrate AttributeMap into TemplateElement (template.ts / index.ts)
  - Add AttributeMapOption constant and type
  - Add attributeMap option to ElementOptions interface
  - TemplateElement.options() stores attributeMap option
  - connectedCallback instantiates AttributeMap when attributeMap === 'all'
  - defineProperties() called after schema is fully populated
- Add tests in attribute-map.spec.ts (browser E2E tests)
- Add fixture in test/fixtures/attribute-map/

Usage:
  TemplateElement.options({
    'my-element': { attributeMap: 'all' },
  });

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…emplate updates

Update AttributeMap.defineProperties() to also push each newly created
attribute name to the existing observedAttributes array on the class.

For all f-template-registered elements, registry.define() (which causes
the browser to cache observedAttributes) is called AFTER defineProperties()
runs, because composeAsync() waits for definition.template to be set
before resolving. This creates a reliable window to mutate the array so
the browser observes the dynamically-added attributes.

Update tests to use element.setAttribute() inside page.evaluate instead
of button clicks, testing both directions:
- setAttribute() → attributeChangedCallback() → property → template re-render
- property assignment → attribute reflection via DOM

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…om attribute-map fixture

Tests no longer need window.Observable or window.__FAST__ to verify
AttributeMap behaviour. Use Object.getOwnPropertyDescriptor to check
that accessor get/set was added to the prototype, and verify attribute
lookup via setAttribute behaviour instead of inspecting internal
registry state.

Also update the beachball change type to prerelease.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu marked this pull request as ready for review March 31, 2026 21:39
janechu and others added 2 commits March 31, 2026 14:45
…by AttributeMap

Add an AttributeMapWithExistingAttrElement fixture element with a
pre-defined @attr foo property (default value 'original'). After
f-template processes with attributeMap: 'all', tests confirm that:
- the @attr default value is preserved (accessor was not re-defined)
- setAttribute() still routes through the original @attr definition

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu marked this pull request as draft April 6, 2026 20:59
janechu and others added 6 commits April 8, 2026 13:13
…ty name in AttributeMap

Replace camelCase conversion with dash-removal: foo-bar attribute → foobar property.
Update fixture template to use {{foo-bar}} binding, update main.ts and all tests.
Fix changefile comment to use feat(fast-html): prefix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…attribute-map

# Conflicts:
#	packages/fast-html/src/components/template.ts
…ME.md

Document the dash-removal convention (foo-bar attribute → foobar property),
leaf-binding eligibility rules, and usage pattern in both files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Dashes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ap fixture

Template bindings should not use dashes. The attribute name can still have
dashes (e.g. foo-bar on the element), but the binding itself uses foobar.
Update fixture template, main.ts, and all spec descriptions accordingly.
Clarify removeDashes JSDoc to reflect the design intent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant